package com.unsins.test.batch;

import org.apache.commons.lang.time.StopWatch;
import org.springframework.batch.core.JobInterruptedException;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.listener.StepExecutionListenerSupport;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

import java.io.IOException;

/**
 * Created by IntelliJ IDEA.
 * User: odpsoft
 * Date: 2008-12-16
 * Time: 14:13:26
 */
public class SystemCommandTasklet extends StepExecutionListenerSupport implements Tasklet, InitializingBean {
    private String command;

    private String[] environmentParams = null;

    private com.unsins.test.batch.SystemProcessExitCodeMapper systemProcessExitCodeMapper = new com.unsins.test.batch.SimpleSystemProcessExitCodeMapper();

    private long timeout = 0;

    private long checkInterval = 1000;

    private StepExecution execution = null;

    /**
     * Execute system command and map its exit code to {@link ExitStatus} using
     * {@link com.unsins.test.batch.SystemProcessExitCodeMapper}.
     */
    public ExitStatus execute() throws Exception {
        ExecutorThread executorThread = new ExecutorThread();
        executorThread.start();

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        while (stopWatch.getTime() < timeout && executorThread.isAlive() && !execution.isTerminateOnly()) {
            Thread.sleep(checkInterval);
        }

        stopWatch.stop();

        if (executorThread.finishedSuccessfully) {
            return systemProcessExitCodeMapper.getExitStatus(executorThread.exitCode);
        } else {
            executorThread.interrupt();
            if (execution.isTerminateOnly()) {
                throw new JobInterruptedException("Job interrupted while executing system command '" + command + "'");
            } else {
                throw new com.unsins.test.batch.SystemCommandException(
                        "Execution of system command failed (did not finish successfully within the timeout)");
            }
        }

    }

    /**
     * @param command command to be executed in a separate system process
     */
    public void setCommand(String command) {
        this.command = command;
    }

    /**
     * @param envp environment parameter values, inherited from parent process
     *             when not set (or set to null).
     */
    public void setEnvironmentParams(String[] envp) {
        this.environmentParams = envp;
    }

    public void afterPropertiesSet() throws Exception {
        Assert.hasLength(command, "'command' property value is required");
        Assert.notNull(systemProcessExitCodeMapper, "SystemProcessExitCodeMapper must be set");
        Assert.isTrue(timeout > 0, "timeout value must be greater than zero");
    }

    /**
     * @param systemProcessExitCodeMapper maps system process return value to
     *                                    <code>ExitStatus</code> returned by Tasklet.
     *                                    {@link com.unsins.test.batch.SimpleSystemProcessExitCodeMapper} is used by default.
     */
    public void setSystemProcessExitCodeMapper(com.unsins.test.batch.SystemProcessExitCodeMapper systemProcessExitCodeMapper) {
        this.systemProcessExitCodeMapper = systemProcessExitCodeMapper;
    }

    /**
     * @param timeout upper limit for how long the execution of the external
     *                program is allowed to last.
     */
    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    /**
     * The time interval how often the tasklet will check for termination
     * status.
     *
     * @param checkInterval time interval in milliseconds (1 second by default).
     */
    public void setTerminationCheckInterval(long checkInterval) {
        this.checkInterval = checkInterval;
    }

    /**
     * Get a reference to {@link StepExecution} for interrupt checks during
     * system command execution.
     */
    public void beforeStep(StepExecution stepExecution) {
        this.execution = stepExecution;
    }

    /**
     * Thread that executes the system command.
     */
    private class ExecutorThread extends Thread {
        volatile int exitCode = -1;

        volatile boolean finishedSuccessfully = false;

        public void run() {
            try {
                Process process = Runtime.getRuntime().exec(command, environmentParams);
                exitCode = process.waitFor();
                finishedSuccessfully = true;
            }
            catch (IOException e) {
                throw new com.unsins.test.batch.SystemCommandException("IO error while executing system command", e);
            }
            catch (InterruptedException e) {
                throw new com.unsins.test.batch.SystemCommandException("Interrupted while executing system command", e);
			}
		}
	}
}
